/*
 * MadHelix is a Java Swing-based GUI frontend for SoundHelix.
 * Copyright (C) 2017-2019, 2023 UltrasonicMadness
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 only,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.ultrasonicmadness.madhelix.concurrency;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
import javax.swing.Timer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import com.soundhelix.component.player.impl.MidiPlayer;
import com.soundhelix.misc.SongContext;
import com.soundhelix.util.SongUtils;
import org.ultrasonicmadness.madhelix.MadHelix;
import org.ultrasonicmadness.madhelix.utils.PathUtils;

public class SoundHelixThread extends Thread {
    public class SongParams {
        public volatile String title = null;
        public volatile String style = null;
        public volatile boolean cued = false;
    }

    private MadHelix madHelix = null;
    private volatile Status helixStatus = Status.STOPPED;
    private SongContext currentSong = null;

    public final SongParams nextSong = new SongParams();

    private Object prefsFileLock = new Object();

    // Preferences
    private volatile File midiOutDir = new File(PathUtils.getMidiDirPath());
    private volatile boolean exportingMidi = false;
    private volatile boolean loggingSongs = true;

    private volatile Timer timer = new Timer(500, new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent ev)
        {
            madHelix.refreshTimer();
        }
    });

    public SoundHelixThread(MadHelix madHelix, String threadName) {
        super(threadName);

        this.madHelix = madHelix;
        setDaemon(true);

        try {
            loadPreferences();
        } catch (Exception x) {
            JOptionPane.showMessageDialog(madHelix, x.getMessage().toString(),
                    "Error loading preferences", JOptionPane.ERROR_MESSAGE);
        }
    }

    private enum Status {
        STOPPED,
        LOADING,
        PLAYING
    }

    public Status getStatus() {
        return helixStatus;
    }

    @Override
    public void run() {
        while (true) {
            if (!nextSong.cued) {
                synchronized(this) {
                    try {
                        this.wait();
                    }
                    catch (InterruptedException ex) {
                        break;
                    }
                }
            }
            this.startSoundHelix();
        }
    }

    public File getMidiOutDir() {
        return midiOutDir;
    }

    public void setMidiOutDir(File midiOutDir) {
        this.midiOutDir = midiOutDir;
    }

    public boolean isExportingMidi() {
        return exportingMidi;
    }

    public void setExportingMidi(boolean exportingMidi) {
        this.exportingMidi = exportingMidi;
    }

    public boolean isLoggingSongs() {
        return loggingSongs;
    }

    public void setLoggingSongs(boolean loggingSongs) {
        this.loggingSongs = loggingSongs;
    }

    public int getTimerInterval() {
        return timer.getDelay();
    }

    public void setTimerInterval(int timerInterval) {
        timer.setDelay(timerInterval);
    }

    public synchronized void playSong() {
        if (helixStatus != Status.LOADING) {
            stopSong();
            this.notify();
        }
    }

    public synchronized void stopSong() {
        if (currentSong != null) {
            currentSong.getPlayer().abortPlay();
        }
    }

    public SongContext getSong() {
        return currentSong;
    }

    public void seek(int ticks) {
        if (helixStatus == Status.PLAYING) {
            currentSong.getPlayer().skipToTick(ticks);
        }
    }

    public int songLength() {
        if (currentSong == null) {
            return 0;
        } else {
            return currentSong.getStructure().getTicks();
        }
    }

    public int getCurrentTick() {
        if (currentSong == null) {
            return 0;
        } else {
            return currentSong.getPlayer().getCurrentTick();
        }
    }

    public int getMilliBPM() {
        if (currentSong == null) {
            return 0;
        } else {
            return currentSong.getPlayer().getMilliBPM();
        }
    }

    public int getTicksPerBeat() {
        if (currentSong == null) {
            return 0;
        } else {
            return currentSong.getStructure().getTicksPerBeat();
        }
    }

    private int tickPositionToSeconds(int tick) {
        if (currentSong != null && helixStatus == Status.PLAYING) {
            return tick * 60000 /
                    (getTicksPerBeat() * getMilliBPM());
        } else {
            return 0;
        }
    }

    private int secondsToTickPosition(int seconds) {
        if (currentSong != null && helixStatus == Status.PLAYING) {
            return seconds * getTicksPerBeat() * getMilliBPM() / 60000;
        } else {
            return 0;
        }
    }

    public int getCurrentPos() {
        return tickPositionToSeconds(getCurrentTick());
    }

    public int getSongTimeDelta(int seconds) {
        return Math.max(0, Math.min(songLength() - 1,
                getCurrentTick() + secondsToTickPosition(seconds)));
    }

    private void setUpMidiExport(String songName, String styleName) {
        try {
            MidiPlayer midiPlayer = (MidiPlayer)currentSong.getPlayer();

            midiPlayer.setMidiFilename(midiOutDir + File.separator +
                    songName + "-" + styleName + ".mid");
        } catch (Exception x) {
            JOptionPane.showMessageDialog(madHelix, x.getMessage().toString(),
                    "Could not set MIDI filename.", JOptionPane.ERROR_MESSAGE);
        }
    }

    public void loadPreferences()
            throws IOException, ParserConfigurationException, SAXException {
        synchronized (prefsFileLock) {
            File preferencesFile = new File(PathUtils.getPrefsPath());

            if (preferencesFile.exists()) {
                DocumentBuilder builder = DocumentBuilderFactory.newInstance()
                        .newDocumentBuilder();
                Element prefsRoot = builder.parse(preferencesFile)
                        .getDocumentElement();

                Node loggingNode = prefsRoot.getElementsByTagName("log-songs").item(0);
                loggingSongs = Boolean.parseBoolean(loggingNode.getTextContent());

                Node timerNode = prefsRoot.getElementsByTagName("timer-interval").item(0);
                setTimerInterval(Integer.parseInt(timerNode.getTextContent()));

                Element midiElement = (Element)prefsRoot.getElementsByTagName("midi").item(0);
                Node exportFilesNode = midiElement.getElementsByTagName("export-files").item(0);
                exportingMidi = Boolean.parseBoolean(exportFilesNode.getTextContent());

                Node outpurDirNode = midiElement.getElementsByTagName("output-directory").item(0);
                midiOutDir = new File(outpurDirNode.getTextContent());
            }
        }
    }

    public void savePreferences() throws IOException, XMLStreamException {
        synchronized (prefsFileLock) {
            FileWriter writer = new FileWriter(PathUtils.getPrefsPath());

            XMLStreamWriter xml = XMLOutputFactory.newFactory()
                    .createXMLStreamWriter(writer);

            xml.writeStartDocument();
            xml.writeCharacters("\n");
            xml.writeStartElement("madhelix-preferences");
            xml.writeCharacters("\n  ");

            xml.writeStartElement("midi");
            xml.writeCharacters("\n    ");
            writeElementWithValue(xml, "export-files", exportingMidi);
            xml.writeCharacters("\n    ");
            writeElementWithValue(xml, "output-directory", midiOutDir.toString());
            xml.writeCharacters("\n  ");
            xml.writeEndElement();
            xml.writeCharacters("\n  ");

            writeElementWithValue(xml, "timer-interval", getTimerInterval());
            xml.writeCharacters("\n  ");
            writeElementWithValue(xml, "log-songs", loggingSongs);
            xml.writeCharacters("\n");

            xml.writeEndElement();
            xml.writeCharacters("\n");
            xml.writeEndDocument();

            xml.close();
            writer.close();
        }
    }

    private void writeElementWithValue(XMLStreamWriter xml, String tagName, Object value) throws XMLStreamException {
        xml.writeStartElement(tagName);
        xml.writeCharacters(value.toString());
        xml.writeEndElement();
    }

    private void startSoundHelix() {
        String songName = nextSong.title;
        String styleName = nextSong.style;
        nextSong.cued = false;

        helixStatus = Status.LOADING;

        madHelix.setStatus("Loading...");
        madHelix.setTitle("Loading... - MadHelix");

        madHelix.setControlsEnabled(false);

        File styleFile = new File(PathUtils.getStylesDirPath() +
                File.separator + styleName + ".xml");

        try {
            // If a song name is specified, use it, otherwise generate a random number and use it.
            if (songName != null && !songName.equals("")) {
                currentSong = SongUtils.generateSong(styleFile.toURI().toURL(),
                        songName);
            } else {
                // This is the same way SoundHelix uses the random number generator.
                currentSong = SongUtils.generateSong(
                        styleFile.toURI().toURL(), new Random().nextLong());
            }

            if (exportingMidi) {
                setUpMidiExport(currentSong.getSongName(), styleName);
            }

            if (loggingSongs) {
                madHelix.logSong(currentSong.getSongName(), styleName);
            }

            madHelix.refreshLength();

            helixStatus = Status.PLAYING;
            timer.start();

            madHelix.setStatus("Playing", currentSong.getSongName(), styleName);
            madHelix.setTitle(currentSong.getSongName() + " - MadHelix");

            madHelix.setControlsEnabled(true);

            currentSong.getPlayer().play(currentSong);
        } catch (NoClassDefFoundError x) {
            JOptionPane.showMessageDialog(madHelix,
                    "Cannot find module " + x.getMessage(), "Error",
                    JOptionPane.ERROR_MESSAGE);

            madHelix.setStatus("Error");
        } catch (Exception x) {
            System.out.println(x.getClass());

            JOptionPane.showMessageDialog(madHelix, x.getMessage().toString(),
                    "Exception occurred", JOptionPane.ERROR_MESSAGE);

            madHelix.setStatus("Error");
        } finally {
            // In case an error has occured, enable the buttons again
            madHelix.setControlsEnabled(true);

            madHelix.setTitle("MadHelix");
            madHelix.zeroTimer();

            helixStatus = Status.STOPPED;
        }

        // The song has finished at this point, so update variables and status accordingly.
        madHelix.setStatus("Ready");
        timer.stop();
    }
}
